﻿using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Collections;
using System.Threading;
using log4net;
using RollBackLib.src.utils;
using System.Net.Mail;


namespace RollBackLib.src.core
{
    /// <summary>
    /// Runs the Backup/Restore operation
    /// </summary>
    public class BackupRestoreRunner
    {
        private static ThreadedBackup m_threadedBackup = new ThreadedBackup();
        private static readonly ILog log = LogManager.GetLogger(typeof(BackupRestoreRunner));
        public JobManager m_JobManager
        {
            get;
            set;
        }
        private Thread m_schedulerThread;
        private bool m_forceThredShutdown = false;
        private AutoResetEvent m_schedulerWaitEvent = new AutoResetEvent(false);
        private BackupInfo.StatusUpdateHandler m_SchedulerThreadUpdateHandler;
        public volatile bool m_keepRunning = true;

        public delegate void Task();

        private List<Task> m_tasks = new List<Task>();

        /// <summary>
        /// true if a backup operation is currently running else false.
        /// </summary>
        public static bool BackupRunning
        {
            get
            {
                bool running = false;
                if (m_threadedBackup != null)
                {
                    running = m_threadedBackup.IsRunning();
                }
                return running;
            }
        }

        public BackupRestoreRunner(BackupInfo.StatusUpdateHandler updateHandler)
        {
            m_JobManager = new JobManager();
            m_SchedulerThreadUpdateHandler = updateHandler;
        }

        public void startSchedulerThread()
        {
            lock (this)
            {
                if (m_schedulerThread == null)
                {
                    m_schedulerThread = new Thread(new ThreadStart(schedulerThread));
                    m_schedulerThread.Name = m_schedulerThread.ManagedThreadId + "_Scheduler_thread";
                    m_schedulerThread.Priority = ThreadPriority.BelowNormal;
                    m_schedulerThread.Start();
                }
            }
        }

        public void startAsync(BackupInfo bInfo)
        {
            if (isRunning())
            {
                throw new BackupException("Backup is already running");
            }
            m_threadedBackup.startBackupJob(bInfo);
        }

        /// <summary>
        /// Pause or Resume the backup job depending on the current state
        /// </summary>
        public void pauseResume()
        {
            if (!m_threadedBackup.IsPaused())
            {
                m_threadedBackup.Pause();
            }
            else
            {
                m_threadedBackup.Resume();
            }
        }


        /// <summary>
        /// return true if the backup/restore job is currently running, else return false
        /// </summary>
        /// <returns></returns>
        public bool isRunning()
        {
            bool ret = m_threadedBackup.IsRunning();
            return ret;
        }

        /// <summary>
        /// Handle start button click via a callback
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void startBackup(BackupInfo binfo)
        {
            try
            {
                binfo.m_Statistics.m_operationMsg = "Preparing <" + binfo.m_Job.m_Id.m_Name + "> for " + binfo.m_Statistics.m_BackupType + " operation";
                PrepareBackupJob(binfo);
                // start the backup thread
                this.startAsync(binfo);
                m_forceThredShutdown = false;
            }
            catch (Exception e)
            {
                binfo.m_Job.registerRunStop();
                binfo.m_LastException = e;
                binfo.m_Statistics.m_operationMsg = "Error preparing <" + binfo.m_Job.m_Id.m_Name + "> for " + binfo.m_Statistics.m_BackupType + " operation";
                throw;
            }
        }

        /// <summary>
        /// Attempt to stop all the threads.
        /// A second call to this function will forcibly shutdown the function.
        /// </summary>
        /// <returns></returns>
        public bool stopAllThreads()
        {
            bool success = true;
            m_keepRunning = false;
            m_schedulerWaitEvent.Set();
            this.stopCurrentBackup();
            Thread.Sleep(250);
            log.Info("Waiting for scheduler thread to join ");
            if (m_schedulerThread != null)
            {
                System.Threading.ThreadState s = m_schedulerThread.ThreadState;
                if (s == System.Threading.ThreadState.Running ||
                    s == System.Threading.ThreadState.WaitSleepJoin)
                {
                    success = m_schedulerThread.Join(50);
                    if (m_forceThredShutdown)
                    {
                        m_schedulerThread.Abort();
                        m_forceThredShutdown = false;
                    }
                }
            }
            log.Info("Finished waiting for scheduler thread Success="+success);
            // turn on the force flag for the next shutdown atempt
            m_forceThredShutdown = true;
            return success;
        }

        /// <summary>
        /// Stop the currently running backup operation
        /// </summary>
        public void stopCurrentBackup()
        {
            m_threadedBackup.Stop();
        }

        /// <summary>
        /// Perform some checks on the backup job before running it.
        /// </summary>
        /// <param name="job"></param>
        private void PrepareBackupJob(BackupInfo binfo)
        {
            BackupJob job = binfo.m_Job;
            DateTime lastRun = job.m_lastRunAttempted;
            job.registerRunStart();

            do
            {
                // process the include/exclude filters
                string exceptionStr = binfo.processIncludeExcludeFilters();
                if (exceptionStr.Length > 0)
                {
                    throw new BackupException("Cannot run " + binfo.m_Statistics.m_BackupType + " for Job   " + job.GetKey() + "  because of errors in include/exclude filters:\r\n" + exceptionStr);
                }

                string dest = job.getFinalDestinationDir();
                if (dest == null)
                {
                    throw new BackupException("Bakcup destination directory is null.");
                }
                List<BackupItem> list = binfo.m_Items;
                log.Info("Preparing job <" + job.m_Id.m_Name + "> " + " for " + binfo.m_Statistics.m_BackupType + " dest=<" + dest + "> for " + list.Count + " items. LastRun=<" + lastRun + ">");
                if (isRunning())
                {
                    throw new BackupException("Backup is already running");
                }

                string key = "Name=<" + binfo.m_Job.m_Id.m_Name + ">  Guid=<" + binfo.m_Job.m_Id.m_Guid.ToString() + ">";
                // check to see if the job is enabled
                if (!job.m_Enabled)
                {
                    throw new BackupException("Job " + key + " is disabled.");
                }

                if (job.m_Deleted)
                {
                   throw new BackupException("Job <" + key + ">  has been deleted.");
                }

                if (dest == null || dest.Trim().Length == 0)
                {
                    throw new BackupException("Destination directory is not specified.\nSelect Settings and browse to the location where you want the files to be backed up");
                }
                try
                {
                    Directory.CreateDirectory(dest);
                }
                catch (Exception e)
                {
                    throw new BackupException("Destination directory <" + dest + ">, could not be created, please check the drive and path.", e);
                }

                FileInfo destFile = new FileInfo(dest);
                string s2 = destFile.ToString();

                // checking to see if the destination directory has been accidentally added as one
                // of the directories to be backed up which will result in an infinite recursion until the disk fills up.
                // For a delete operation the destination directory can be part of the operation since it needs to get deleted.
                foreach (BackupItem backupItem in list)
                {
                    string s = backupItem.m_path;
                    FileInfo finfo = new FileInfo(s);
                    string s1 = finfo.FullName;
                    if (s2.StartsWith(s1, StringComparison.OrdinalIgnoreCase))
                    {
                        throw new BackupException("Destination directory <" + destFile.FullName + "> cannot be part of backup file list ");
                    }
                }
            } while (false);
        }

        /// <summary>
        /// Schedules any backup jobs
        /// </summary>
        private void schedulerThread()
        {
            log.Info("Starting scheduler thread");
            int maxWaitTime = (1 * 1000 * 60);
            const int waitPerLoop = 100;
            while (m_keepRunning)
            {
                int totalWaitTime = 0;
                while(totalWaitTime < maxWaitTime) {
                    if (m_schedulerWaitEvent.WaitOne(waitPerLoop))
                    {
                        break;
                    }
                    if (!m_keepRunning)
                    {
                        // signal to get out of the scheduling loop
                        break;
                    }
                    totalWaitTime += waitPerLoop;
                }

                if(!m_keepRunning) 
                {
                    break;
                }
                lock (m_tasks)
                {
                    // Execute all the requested task
                    foreach (Task task in m_tasks)
                    {
                        try
                        {
                            task();
                        }
                        catch (Exception e)
                        {
                            log.Warn("Error running task", e);
                        }
                    }
                    m_tasks.Clear();
                }

                if (m_JobManager != null && m_JobManager.m_BackupJobList != null)
                {
                    BackupInfo binfo = null;
                    try
                    {
                        BackupInfo binfo2 = new BackupInfo(BackupInfo.BackupType.SCHEDULED_BACKUP, m_SchedulerThreadUpdateHandler);
                        ThreadedBackup.notifyOperation(binfo2, CurrentOperation.ScheduledSave, null, "Scheduled save", 0, false);

                        // collect the list of jobs that need to be scheduled
                        List<BackupJob> jobsToRun = new List<BackupJob>();
                        lock (m_JobManager.m_BackupJobList)
                        {
                            foreach (BackupJob job in m_JobManager.m_BackupJobList)
                            {
                                if (job.timeToSchedule())
                                {
                                    jobsToRun.Add(job);
                                }
                            }
                        }

                        // run the collected list of scheduled jobs
                        if (jobsToRun.Count > 0)
                        {
                            log.Debug("Scheduler found " + jobsToRun.Count + " Backup jobs which need to be run");
                            foreach (BackupJob job in jobsToRun)
                            {
                                lock (m_JobManager.m_BackupJobList)
                                {
                                    if (!this.isRunning())
                                    {
                                        log.Info("Time to run scheduled backup job <" + job.m_Id.m_Name + ">, last run was on <" + job.m_lastRunAttempted + ">");
                                        string destDir = job.getFinalDestinationDir();
                                        binfo = new BackupInfo(
                                            job.m_BackupPathList,
                                            destDir,
                                            BackupInfo.BackupType.SCHEDULED_BACKUP /*restore mode*/,
                                            job,
                                            m_SchedulerThreadUpdateHandler);
                                        startBackup(binfo);
                                    }
                                }
                                if (binfo != null)
                                {
                                    m_threadedBackup.waitForJobToFinish();
                                    log.Debug("Finished scheduled operation");
                                }
                                binfo = null;
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        //msg += "\nRemoving job from scheduler, please reschedule after fixing the issue";
                        if (binfo != null)
                        {
                            binfo.m_LastException = e;
                            string msg = "Scheduler could not run Backup Job " + (binfo.m_Job != null ? "<" + binfo.m_Job.m_Id.m_Name + ">" : "");
                            log.Warn(msg);
                            binfo.callUpdateHandler();
                        }
                        else
                        {
                            log.Error("Error running scheduled backup job", e);
                        }
                    }
                }
            }
            log.Info("Finishing scheduler thread");
        }

        public string getRootFileSavePath()
        {
            return m_JobManager.m_RootPath;
        }

        public void waitForJobToFinish()
        {
            m_threadedBackup.waitForJobToFinish();
        }

        public void addTask(Task task)
        {
            lock (m_tasks)
            {
                m_tasks.Add(task);
                m_schedulerWaitEvent.Set();
            }
        }
    }
}
